// 版权归Go作者所有。版权所有。
// 此源代码的使用受BSD样式的约束
// 可以在许可证文件中找到的许可证。

// go:构建aix | |（solaris&&！illumos）
// +构建aix solaris，！illumos

// 这段代码使用POSIX“fcntl”锁实现FileLockAPI，它附加
// 指向（inode，process）对，而不是文件描述符。避免解锁
// 当通过不同的描述符打开同一个文件时，会过早地打开文件，
// 我们一次只允许一个读锁。
// None
// 大多数平台都提供了一些可选的API，比如“flock”系统调用
// 或“fcntl”的F_of d_SETLK命令，该命令允许更好的并发性和
// 不需要在应用程序中按inode记账。

package filelock

import (
	"errors"
	"io"
	"io/fs"
	"math/rand"
	"sync"
	"syscall"
	"time"
)

type lockType int16

const (
	readLock  lockType = syscall.F_RDLCK
	writeLock lockType = syscall.F_WRLCK
)

type inode = uint64 // syscall.Stat\u t.Ino的类型

type inodeLock struct {
	owner File
	queue []<-chan File
}

type token struct{}

var (
	mu     sync.Mutex
	inodes = map[File]inode{}
	locks  = map[inode]inodeLock{}
)

func lock(f File, lt lockType) (err error) {
	// POSIX锁适用于每个inode和进程，inode的锁是
	// 当该inode的*any*描述符关闭时释放。所以我们需要
	// 在内部同步对每个inode的访问，并且必须序列化锁和
	// 通过不同的描述符解锁引用相同inode的调用。
	fi, err := f.Stat()
	if err != nil {
		return err
	}
	ino := fi.Sys().(*syscall.Stat_t).Ino

	mu.Lock()
	if i, dup := inodes[f]; dup && i != ino {
		mu.Unlock()
		return &fs.PathError{
			Op:   lt.String(),
			Path: f.Name(),
			Err:  errors.New("inode for file changed since last Lock or RLock"),
		}
	}
	inodes[f] = ino

	var wait chan File
	l := locks[ino]
	if l.owner == f {
		// 此文件已拥有锁，但调用可能会更改其锁类型。
	} else if l.owner == nil {
		// 没有主人：现在是我们的了。
		l.owner = f
	} else {
		// 已拥有：添加要等待的频道。
		wait = make(chan File)
		l.queue = append(l.queue, wait)
	}
	locks[ino] = l
	mu.Unlock()

	if wait != nil {
		wait <- f
	}

	// 在计算死锁图的平台上会出现虚假的EDEADLK错误
	// 进程级别，而不是线程级别。考虑进程p和q，用
	// 螺纹P.1、P.2和Q.3。以下跟踪不是死锁，但将是
	// 报告只考虑进程粒度的系统死锁：
	// None
	// P.1锁定文件A。
	// Q.3锁定文件B。
	// Q.3文件A上的块。
	// 文件B上的P.2块。（这被错误地报告为死锁。）
	// P.1解锁文件A。
	// Q.3解除阻止和锁定文件A。
	// Q.3解锁文件A和B。
	// P.2解除阻止和锁定文件B。
	// P.2解锁文件B。
	// None
	// 在AIX和Solaris上的实践中观察到了这些虚假错误
	// cmd/go：请参阅https:
	// None
	// 我们通过将EDEADLK视为总是虚假的来解决这个bug。如果有
	// 实际上，这是交互进程之间的锁排序错误，它将
	// 相反，成为一个活锁，但这并没有明显比我们之前更糟
	// 一个适当的flock实现（通常不会尝试
	// 诊断死锁）。
	// None
	// 在上面的示例中，这将跟踪更改为：
	// None
	// P.1锁定文件A。
	// Q.3锁定文件B。
	// Q.3文件A上的块。
	// P.2错误地未能锁定文件B并进入睡眠状态。
	// P.1解锁文件A。
	// Q.3解除阻止和锁定文件A。
	// Q.3解锁文件A和B。
	// P.2唤醒并锁定文件B。
	// P.2解锁文件B。
	// None
	// 我们知道重试循环不会引入“伪”活锁
	// 因为，根据POSIX规范，EDEADLK只需要
	// 当“锁被另一进程的锁阻止”时返回。
	// 如果该进程在我们持有的某个锁上被阻止，那么
	// 产生的livelock是由于一个真正的死锁造成的（并且会表现为这样）
	// 例如，使用此软件包的flock实现时）。
	// 如果另一个进程*未*在我们正在使用的其他锁上被阻止
	// 按住，则最终将释放请求的锁。

	nextSleep := 1 * time.Millisecond
	const maxSleep = 500 * time.Millisecond
	for {
		err = setlkw(f.Fd(), lt)
		if err != syscall.EDEADLK {
			break
		}
		time.Sleep(nextSleep)

		nextSleep += nextSleep
		if nextSleep > maxSleep {
			nextSleep = maxSleep
		}
		// 当我们最终解除阻塞时，应用10%的抖动以避免同步冲突。
		nextSleep += time.Duration((0.1*rand.Float64() - 0.05) * float64(nextSleep))
	}

	if err != nil {
		unlock(f)
		return &fs.PathError{
			Op:   lt.String(),
			Path: f.Name(),
			Err:  err,
		}
	}

	return nil
}

func unlock(f File) error {
	var owner File

	mu.Lock()
	ino, ok := inodes[f]
	if ok {
		owner = locks[ino].owner
	}
	mu.Unlock()

	if owner != f {
		panic("unlock called on a file that is not locked")
	}

	err := setlkw(f.Fd(), syscall.F_UNLCK)

	mu.Lock()
	l := locks[ino]
	if len(l.queue) == 0 {
		// 无服务员：删除地图条目。
		delete(locks, ino)
	} else {
		// 第一个服务员正在把他们的档案寄给我们。
		// 接收并更新队列。
		l.owner = <-l.queue[0]
		l.queue = l.queue[1:]
		locks[ino] = l
	}
	delete(inodes, f)
	mu.Unlock()

	return err
}

// setlkw使用F_setlkw为fd指示的整个文件调用FcntlFlock。
func setlkw(fd uintptr, lt lockType) error {
	for {
		err := syscall.FcntlFlock(fd, syscall.F_SETLKW, &syscall.Flock_t{
			Type:   int16(lt),
			Whence: io.SeekStart,
			Start:  0,
			Len:    0, // 所有字节。
		})
		if err != syscall.EINTR {
			return err
		}
	}
}

func isNotSupported(err error) bool {
	return err == syscall.ENOSYS || err == syscall.ENOTSUP || err == syscall.EOPNOTSUPP || err == ErrNotSupported
}
